Passed
Branch wavefile-reader (b7dc7a)
by Rafael S.
02:25
created

WaveFile.fromBase64   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import {encode, decode} from 'base64-arraybuffer-es6';
33
import WaveFileConverter from './lib/wavefile-converter';
34
import fixRIFFTag from './lib/fix-riff-tag';
35
36
/**
37
 * A class to manipulate wav files.
38
 * @extends WaveFileConverter
39
 */
40
export default class WaveFile extends WaveFileConverter {
41
42
  /**
43
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
44
   * @param {string} base64String A .wav file as a base64 string.
45
   * @throws {Error} If any property of the object appears invalid.
46
   */
47
  fromBase64(base64String) {
48
    this.fromBuffer(new Uint8Array(decode(base64String)));
49
  }
50
51
  /**
52
   * Return a base64 string representig the WaveFile object as a .wav file.
53
   * @return {string} A .wav file as a base64 string.
54
   * @throws {Error} If any property of the object appears invalid.
55
   */
56
  toBase64() {
57
    /** @type {!Uint8Array} */
58
    let buffer = this.toBuffer();
59
    return encode(buffer, 0, buffer.length);
60
  }
61
62
  /**
63
   * Return a DataURI string representig the WaveFile object as a .wav file.
64
   * The return of this method can be used to load the audio in browsers.
65
   * @return {string} A .wav file as a DataURI.
66
   * @throws {Error} If any property of the object appears invalid.
67
   */
68
  toDataURI() {
69
    return 'data:audio/wav;base64,' + this.toBase64();
70
  }
71
72
  /**
73
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
74
   * @param {string} dataURI A .wav file as DataURI.
75
   * @throws {Error} If any property of the object appears invalid.
76
   */
77
  fromDataURI(dataURI) {
78
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
79
  }
80
81
  /**
82
   * Return the value of a RIFF tag in the INFO chunk.
83
   * @param {string} tag The tag name.
84
   * @return {?string} The value if the tag is found, null otherwise.
85
   */
86
  getTag(tag) {
87
    /** @type {!Object} */
88
    let index = this.getTagIndex_(tag);
89
    if (index.TAG !== null) {
90
      return this.LIST[index.LIST].subChunks[index.TAG].value;
91
    }
92
    return null;
93
  }
94
95
  /**
96
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
97
   * then it is created. It if exists, it is overwritten.
98
   * @param {string} tag The tag name.
99
   * @param {string} value The tag value.
100
   * @throws {Error} If the tag name is not valid.
101
   */
102
  setTag(tag, value) {
103
    tag = fixRIFFTag(tag);
104
    /** @type {!Object} */
105
    let index = this.getTagIndex_(tag);
106
    if (index.TAG !== null) {
107
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
108
        value.length + 1;
109
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
110
    } else if (index.LIST !== null) {
111
      this.LIST[index.LIST].subChunks.push({
112
        chunkId: tag,
113
        chunkSize: value.length + 1,
114
        value: value});
115
    } else {
116
      this.LIST.push({
117
        chunkId: 'LIST',
118
        chunkSize: 8 + value.length + 1,
119
        format: 'INFO',
120
        subChunks: []});
121
      this.LIST[this.LIST.length - 1].subChunks.push({
122
        chunkId: tag,
123
        chunkSize: value.length + 1,
124
        value: value});
125
    }
126
  }
127
128
  /**
129
   * Remove a RIFF tag from the INFO chunk.
130
   * @param {string} tag The tag name.
131
   * @return {boolean} True if a tag was deleted.
132
   */
133
  deleteTag(tag) {
134
    /** @type {!Object} */
135
    let index = this.getTagIndex_(tag);
136
    if (index.TAG !== null) {
137
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
138
      return true;
139
    }
140
    return false;
141
  }
142
143
  /**
144
   * Return a Object<tag, value> with the RIFF tags in the file.
145
   * @return {!Object<string, string>} The file tags.
146
   */
147
  listTags() {
148
    /** @type {?number} */
149
    let index = this.getLISTINFOIndex_();
150
    /** @type {!Object} */
151
    let tags = {};
152
    if (index !== null) {
153
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
154
        tags[this.LIST[index].subChunks[i].chunkId] =
155
          this.LIST[index].subChunks[i].value;
156
      }
157
    }
158
    return tags;
159
  }
160
161
  /**
162
   * Return an array with all cue points in the file, in the order they appear
163
   * in the file.
164
   * The difference between this method and using the list in WaveFile.cue
165
   * is that the return value of this method includes the position in
166
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
167
   * @return {!Array<!Object>}
168
   */
169
  listCuePoints() {
170
    /** @type {!Array<!Object>} */
171
    let points = this.getCuePoints_();
172
    for (let i = 0, len = points.length; i < len; i++) {
173
      points[i].milliseconds =
174
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
175
    }
176
    return points;
177
  }
178
179
  /**
180
   * Create a cue point in the wave file.
181
   * @param {number} position The cue point position in milliseconds.
182
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
183
   */
184
  setCuePoint(position, labl='') {
185
    this.cue.chunkId = 'cue ';
186
    position = (position * this.fmt.sampleRate) / 1000;
187
    /** @type {!Array<!Object>} */
188
    let existingPoints = this.getCuePoints_();
189
    this.clearLISTadtl_();
190
    /** @type {number} */
191
    let len = this.cue.points.length;
192
    this.cue.points = [];
193
    /** @type {boolean} */
194
    let hasSet = false;
195
    if (len === 0) {
196
      this.setCuePoint_(position, 1, labl);
197
    } else {
198
      for (let i = 0; i < len; i++) {
199
        if (existingPoints[i].dwPosition > position && !hasSet) {
200
          this.setCuePoint_(position, i + 1, labl);
201
          this.setCuePoint_(
202
            existingPoints[i].dwPosition,
203
            i + 2,
204
            existingPoints[i].label);
205
          hasSet = true;
206
        } else {
207
          this.setCuePoint_(
208
            existingPoints[i].dwPosition,
209
            i + 1,
210
            existingPoints[i].label);
211
        }
212
      }
213
      if (!hasSet) {
214
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
215
      }
216
    }
217
    this.cue.dwCuePoints = this.cue.points.length;
218
  }
219
220
  /**
221
   * Remove a cue point from a wave file.
222
   * @param {number} index the index of the point. First is 1,
223
   *    second is 2, and so on.
224
   */
225
  deleteCuePoint(index) {
226
    this.cue.chunkId = 'cue ';
227
    /** @type {!Array<!Object>} */
228
    let existingPoints = this.getCuePoints_();
229
    this.clearLISTadtl_();
230
    /** @type {number} */
231
    let len = this.cue.points.length;
232
    this.cue.points = [];
233
    for (let i = 0; i < len; i++) {
234
      if (i + 1 !== index) {
235
        this.setCuePoint_(
236
          existingPoints[i].dwPosition,
237
          i + 1,
238
          existingPoints[i].label);
239
      }
240
    }
241
    this.cue.dwCuePoints = this.cue.points.length;
242
    if (this.cue.dwCuePoints) {
243
      this.cue.chunkId = 'cue ';
244
    } else {
245
      this.cue.chunkId = '';
246
      this.clearLISTadtl_();
247
    }
248
  }
249
250
  /**
251
   * Update the label of a cue point.
252
   * @param {number} pointIndex The ID of the cue point.
253
   * @param {string} label The new text for the label.
254
   */
255
  updateLabel(pointIndex, label) {
256
    /** @type {?number} */
257
    let cIndex = this.getAdtlChunk_();
258
    if (cIndex !== null) {
259
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
260
        if (this.LIST[cIndex].subChunks[i].dwName ==
261
            pointIndex) {
262
          this.LIST[cIndex].subChunks[i].value = label;
263
        }
264
      }
265
    }
266
  }
267
268
  /**
269
   * Return an array with all cue points in the file, in the order they appear
270
   * in the file.
271
   * @return {!Array<!Object>}
272
   * @private
273
   */
274
  getCuePoints_() {
275
    /** @type {!Array<!Object>} */
276
    let points = [];
277
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
278
      points.push({
279
        dwPosition: this.cue.points[i].dwPosition,
280
        label: this.getLabelForCuePoint_(
281
          this.cue.points[i].dwName)});
282
    }
283
    return points;
284
  }
285
286
  /**
287
   * Return the label of a cue point.
288
   * @param {number} pointDwName The ID of the cue point.
289
   * @return {string}
290
   * @private
291
   */
292
  getLabelForCuePoint_(pointDwName) {
293
    /** @type {?number} */
294
    let cIndex = this.getAdtlChunk_();
295
    if (cIndex !== null) {
296
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
297
        if (this.LIST[cIndex].subChunks[i].dwName ==
298
            pointDwName) {
299
          return this.LIST[cIndex].subChunks[i].value;
300
        }
301
      }
302
    }
303
    return '';
304
  }
305
306
  /**
307
   * Return the index of the INFO chunk in the LIST chunk.
308
   * @return {?number} the index of the INFO chunk.
309
   * @private
310
   */
311
  getLISTINFOIndex_() {
312
    /** @type {?number} */
313
    let index = null;
314
    for (let i = 0, len = this.LIST.length; i < len; i++) {
315
      if (this.LIST[i].format === 'INFO') {
316
        index = i;
317
        break;
318
      }
319
    }
320
    return index;
321
  }
322
323
  /**
324
   * Return the index of the 'adtl' LIST in this.LIST.
325
   * @return {?number}
326
   * @private
327
   */
328
  getAdtlChunk_() {
329
    for (let i = 0, len = this.LIST.length; i < len; i++) {
330
      if (this.LIST[i].format == 'adtl') {
331
        return i;
332
      }
333
    }
334
    return null;
335
  }
336
337
  /**
338
   * Return the index of a tag in a FILE chunk.
339
   * @param {string} tag The tag name.
340
   * @return {!Object<string, ?number>}
341
   *    Object.LIST is the INFO index in LIST
342
   *    Object.TAG is the tag index in the INFO
343
   * @private
344
   */
345
  getTagIndex_(tag) {
346
    /** @type {!Object<string, ?number>} */
347
    let index = {LIST: null, TAG: null};
348
    for (let i = 0, len = this.LIST.length; i < len; i++) {
349
      if (this.LIST[i].format == 'INFO') {
350
        index.LIST = i;
351
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
352
          if (this.LIST[i].subChunks[j].chunkId == tag) {
353
            index.TAG = j;
354
            break;
355
          }
356
        }
357
        break;
358
      }
359
    }
360
    return index;
361
  }
362
363
  /**
364
   * Push a new cue point in this.cue.points.
365
   * @param {number} position The position in milliseconds.
366
   * @param {number} dwName the dwName of the cue point
367
   * @private
368
   */
369
  setCuePoint_(position, dwName, label) {
370
    this.cue.points.push({
371
      dwName: dwName,
372
      dwPosition: position,
373
      fccChunk: 'data',
374
      dwChunkStart: 0,
375
      dwBlockStart: 0,
376
      dwSampleOffset: position,
377
    });
378
    this.setLabl_(dwName, label);
379
  }
380
381
  /**
382
   * Clear any LIST chunk labeled as 'adtl'.
383
   * @private
384
   */
385
  clearLISTadtl_() {
386
    for (let i = 0, len = this.LIST.length; i < len; i++) {
387
      if (this.LIST[i].format == 'adtl') {
388
        this.LIST.splice(i);
389
      }
390
    }
391
  }
392
393
  /**
394
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
395
   * @param {number} dwName The ID of the cue point.
396
   * @param {string} label The label for the cue point.
397
   * @private
398
   */
399
  setLabl_(dwName, label) {
400
    /** @type {?number} */
401
    let adtlIndex = this.getAdtlChunk_();
402
    if (adtlIndex === null) {
403
      this.LIST.push({
404
        chunkId: 'LIST',
405
        chunkSize: 4,
406
        format: 'adtl',
407
        subChunks: []});
408
      adtlIndex = this.LIST.length - 1;
409
    }
410
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
411
  }
412
413
  /**
414
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
415
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
416
   * @param {number} dwName The ID of the cue point.
417
   * @param {string} label The label for the cue point.
418
   * @private
419
   */
420
  setLabelText_(adtlIndex, dwName, label) {
421
    this.LIST[adtlIndex].subChunks.push({
422
      chunkId: 'labl',
423
      chunkSize: label.length,
424
      dwName: dwName,
425
      value: label
426
    });
427
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
428
  }
429
}
430